package com.black.utils.encrypt.sm4;


import com.black.utils.config.loader.PropertiesLoader;
import com.black.utils.converter.ByteConverter;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
public class SM4Util {
    //随机128位   16字节
    //随机16字符   加密解密需同一套iv与key
    private static Config config;
    private static final String CHARSET = "GBK";
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    static {
        try {
            // 添加Bouncy Castle作为安全提供者
            Security.addProvider(new BouncyCastleProvider());
            config = PropertiesLoader.loadConfig("/sm4.properties", Config.class);
        } catch (IOException e) {
            log.error("读取sm4.properties文件失败", e);
        }
    }

    /**
     * 使用CBC模式加密数据。
     * 此方法是一个重载方法，它调用另一个带有密钥参数的encryptData_CBC方法来执行加密操作。
     * 加密过程使用了配置中的密码。
     *
     * @param plainText 需要加密的明文字符串。
     * @return 加密后的字符串。
     */
    public static String encryptData_CBC(String plainText) {
        // 调用带密钥参数的encryptData_CBC方法，使用配置中的密码进行加密
        return encryptData_CBC(plainText, config.key, config.iv);
    }


    /**
     * 使用SM4算法对明文进行加密。
     * 加密采用CBC模式，使用预设的密钥和初始化向量。
     *
     * @param plainText 明文字符串
     * @param secretKey 密钥字符串
     * @return 返回加密后的密文字符串，如果加密过程中发生异常，则返回null
     */
    public static String encryptData_CBC(String plainText, String secretKey, String iv) {
        try {
            // 初始化SM4上下文，设置为加密模式并开启填充
            SM4Context ctx = new SM4Context(secretKey, iv);
            // 初始化SM4加密对象
            SM4 sm4 = new SM4();
            // 设置加密密钥
            sm4.SM4SetKeyEnc(ctx);
            // 对明文进行加密，返回加密后的字节数组
            byte[] encrypted = sm4.SM4CryptCbc(ctx, plainText.getBytes(CHARSET));
            // 将加密后的字节数组转换为密文字符串返回
            return getCipherText(encrypted);
        } catch (Exception e) {
            // 记录加密过程中的异常信息
            log.error(e.getMessage());
            return null;
        }
    }


    /**
     * 使用CBC模式解密数据。
     * 此方法是一个简化的解密入口，它通过调用另一个带有额外参数的解密方法来实现。
     * 主要用于在解密过程中，保持代码的封装性和易用性。
     *
     * @param cipherText 加密后的文本，使用CBC模式加密。
     * @return 解密后的原始文本。
     */
    public static String decryptData_CBC(String cipherText) {
        // 使用配置文件中的密码进行解密
        return decryptData_CBC(cipherText, config.key, config.iv);
    }


    /**
     * 使用SM4算法对密文进行解密。
     *
     * @param cipherText 加密后的文本，使用Base64编码。
     * @param secretKey  解密所需的密钥，字符串形式。
     * @return 解密后的原始文本。
     */
    public static String decryptData_CBC(String cipherText, String secretKey, String iv) {
        try {
            // 初始化SM4上下文，配置为解密模式并启用填充。
            SM4Context ctx = new SM4Context(secretKey, iv);
            // 初始化SM4加密/解密实例。
            SM4 sm4 = new SM4();
            // 设置解密密钥。
            sm4.SM4SetKeyDec(ctx);
            // 解密密文，返回解密后的字节数组。
            byte[] decrypted = sm4.SM4CryptCbc(ctx, Base64.getDecoder().decode(cipherText));
            // 将解密后的字节数组转换为字符串并返回。
            return new String(decrypted, CHARSET);
        } catch (Exception e) {
            // 记录解密过程中的异常信息。
            log.error(e.getMessage());
            // 解密失败时返回null。
            return null;
        }
    }


    /**
     * 使用ECB模式加密数据。
     * 此方法是一个重载方法，它调用另一个带有密钥参数的加密方法。
     *
     * @param plainText 需要加密的明文字符串。
     * @return 加密后的字符串。
     */
    public static String encryptData_ECB(String plainText) {
        return encryptData_ECB(plainText, config.key);
    }


    /**
     * 使用SM4算法对明文进行加密。
     * <p>
     * 加密采用ECB模式，确保每个块独立加密，增加加密的灵活性。
     * 使用预设的字符集对明文进行字节转换，以适应加密操作。
     *
     * @param plainText 明文字符串，需要进行加密保护。
     * @param secretKey 私钥字符串，用于加密和解密操作。
     * @return 返回加密后的密文字符串，如果加密过程中发生异常，则返回null。
     */
    public static String encryptData_ECB(String plainText, String secretKey) {
        try {
            // 初始化SM4上下文，配置为加密模式并启用填充。
            SM4Context ctx = new SM4Context(secretKey);
            // 初始化SM4加密实例。
            SM4 sm4 = new SM4();
            // 设置加密使用的密钥。
            sm4.SM4SetKeyEnc(ctx);
            // 对明文进行加密，返回加密后的字节数组。
            byte[] encrypted = sm4.SM4CryptEcb(ctx, plainText.getBytes(CHARSET));
            // 将加密后的字节数组转换为密文字符串返回。
            return getCipherText(encrypted);
        } catch (Exception e) {
            // 记录加密过程中发生的异常信息。
            log.error(e.getMessage());
            // 异常情况下返回null。
            return null;
        }
    }


    /**
     * 使用ECB模式解密数据。
     * 此方法是一个简化的解密接口，它通过调用另一个带有密钥参数的解密方法来实现解密。
     * 主要用于在已知配置（如密钥）的情况下，对通过ECB模式加密的文本进行解密。
     *
     * @param cipherText 加密后的文本。
     * @return 解密后的原始文本。
     */
    public static String decryptData_ECB(String cipherText) {
        // 使用配置中的密钥对密文进行解密
        return decryptData_ECB(cipherText, config.key);
    }


    /**
     * 使用SM4算法解密数据。
     * <p>
     * 本函数通过ECB模式对密文进行解密，使用预设的密钥。解密过程中采用PKCS7填充方式。
     *
     * @param cipherText 待解密的密文，使用Base64编码。
     * @param secretKey  解密使用的密钥，字符串形式。
     * @return 解密后的明文，以字符串形式返回。
     */
    public static String decryptData_ECB(String cipherText, String secretKey) {
        try {
            // 初始化SM4上下文，设置为解密模式并开启PKCS7填充。
            SM4Context ctx = new SM4Context(secretKey);

            // 初始化SM4实例，设置解密密钥。
            SM4 sm4 = new SM4();
            sm4.SM4SetKeyDec(ctx);

            // 解密密文，首先将Base64编码的密文解码，然后进行SM4解密。
            byte[] decrypted = sm4.SM4CryptEcb(ctx, Base64.getDecoder().decode(cipherText));

            // 将解密后的字节数组转换为字符串返回，使用预设的字符集。
            return new String(decrypted, CHARSET);
        } catch (Exception e) {
            // 记录异常信息。
            log.error(e.getMessage());
            // 解密失败，返回null。
            return null;
        }
    }


    /**
     * 将加密后的字节序列转换为Base64编码的字符串。
     * 该方法主要用于加密数据的传输或存储，通过Base64编码确保数据的安全性和可读性。
     *
     * @param encrypted 加密后的字节序列。
     * @return 经过Base64编码的字符串。如果输入为空或空数组，则返回空字符串。
     */
    public static String getCipherText(byte[] encrypted) {
        // 使用Base64编码将字节序列转换为字符串
        String cipherText = new String(Base64.getEncoder().encode(encrypted));

        // 检查编码后的字符串是否为空或仅包含空白字符
        if (!cipherText.trim().isEmpty()) {
            // 定义正则表达式，用于匹配并移除字符串中的空格、制表符、回车和换行符
            Pattern p = Pattern.compile("\\s*|\t|\r|\n");
            Matcher m = p.matcher(cipherText);
            // 替换字符串中的所有空白字符为空字符串，以确保编码字符串的紧凑性
            cipherText = m.replaceAll("");
        }

        // 返回处理后的编码字符串
        return cipherText;
    }

    /**
     * 生成SM4对称加密算法的密钥。
     * SM4是一种由中国国家密码管理局公布的一种对称加密算法，广泛应用于数据加解密领域。
     * 本方法使用Bouncy Castle提供的SM4算法实现，生成长度为128位的密钥。
     *
     * @return 生成的SM4密钥的字节数组表示。
     */
    public static String generateSM4Key() {
        try {
            // 实例化SM4算法的密钥生成器
            KeyGenerator keyGenerator = KeyGenerator.getInstance("SM4", "BC");
            // 初始化密钥生成器，设置密钥长度为128位，使用SecureRandom产生随机数
            keyGenerator.init(64, SECURE_RANDOM);
            // 生成密钥
            SecretKey secretKey = keyGenerator.generateKey();
            // 返回密钥的字节数组表示
            byte[] key = secretKey.getEncoded();
            return ByteConverter.bytesToHexString(key);
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 生成SM4加密算法所需的16字节随机初始化向量（IV）。
     * <p>
     * SM4是一种中国国家密码管理局批准的对称加密算法，广泛应用于数据加解密领域。
     * 初始化向量（IV）在加密过程中起着重要作用，它使得相同的关键可以加密出不同的密文，
     * 增加了加密的随机性和安全性。
     *
     * @return 生成的16字节随机初始化向量。
     */
    public static String generateSM4IV() {
        // 初始化一个16字节长度的数组用于存放生成的随机IV
        byte[] iv = new byte[8]; // 16字节的初始化向量
        // 使用SecureRandom生成安全随机数填充到iv数组中
        SECURE_RANDOM.nextBytes(iv);
        return ByteConverter.bytesToHexString(iv);
    }

    @Setter
    @Getter
    public static class Config {
        private String key;
        private String iv;
    }
}
